/**
 *  $Id: selectbox.js 247 2007-04-03 16:59:49Z wingedfox $
 *  $HeadURL: https://svn.debugger.ru/repos/jslibs/BrowserExtensions/tags/BrowserExtensions.011/dom/selectbox.js $
 *
 *  Extends the selectbox interface
 *
 *  @author Ilya Lebedev <ilya@lebedev.net>
 *  @modified $Date: 2007-04-03 20:59:49 +0400 (Втр, 03 Апр 2007) $
 *  @version $Rev: 247 $
 *  @title Selectbox
 *  @license LGPL 2.1 or later
 */
// ===================================================================
//
// Author: Matt Kruse <matt@mattkruse.com>
// Author: Ilya Lebedev <ilya@lebedev.net>
// http://debugger.ru
//
// NOTICE: You may use this code for any purpose, commercial or
// private, without any further permission from the author. You may
// remove this notice from your final code if you wish, however it is
// appreciated by the author if at least my web site address is kept.
//
// You may *NOT* re-distribute this code in any way except through its
// use. That means, you can include it in your product, or your web
// site, or any other form where the code is actually being used. You
// may not put the plain javascript up on your site for download or
// include it in your javascript libraries for download. 
// If you wish to share this code with others, please just point them
// to the URL instead.
// Please DO NOT link directly to my .js files from your site. Copy
// the files to your server and use them there. Thank you.
// ===================================================================


// HISTORY
// ------------------------------------------------------------------
// October 2, 2006: Rewritten in the OOP style and added number of useful features
// June 12, 2003: Modified up and down functions to support more than
//                one selected option
/*
COMPATIBILITY: These are fairly basic functions - they should work on
all browsers that support Javascript.
*/

/**
 *  Provides control component for the select box
 *
 *  @class
 *  @param {String} id select box to attach control to
 *  @scope public
 */
Selectbox = function (id /*: String*/) {
  var self = this;
  /**
   *  Selectbox node
   *
   *  @type HTMLSelectElement
   *  @scope private
   */
  var node;
  /**
   *  List of all the options in the list
   *  Format of the array elements
   *  { 'value'    : option value,
   *    'text'     : option text,
   *    'defaultSelected' : default selection flag,
   *    'selected'  : selection flag,
   *    'visible'   : visible flag,
   *    'fixed'     : fixes position in the list
   *  }
   *
   *  @type Array
   *  @scope private
   */
  var options = [];
  /**
   * Trackers does keep a track of the options
   * 
   * @type Object
   * @scope private
   */
  var trackers = {
      'added' : [],
      'deleted' : []
  }
  /**
   *  Visible options list will not exceed this value
   *  
   *  @type Number
   *  @scope private
   */
  var maxlistlen = 1000;
  /**
   *  @constructor
   */
  var __construct = function (id) /* :void */{
    node = document.getElementById(id) || id;
    if (node && node.tagName.toLowerCase() != 'select') 
      throw new Error('Node with supplied id="'+id+'" is not the <select> box.')
    /*
    *  if node is not exists, create it.
    */
    if (!node) {
        node = document.createElement('select');
        node.id = 'selectbox'+(new Date).valueOf();
    } else if (node.options.length>0) {
        self.addOptionsList(node.options, false, false);
    }
    node.onblur = __storeOptionsState;
  }
  //---------------------------------------------------------------------------
  // Private methods
  //---------------------------------------------------------------------------
  /**
   * Special 'toString' method, allowing to have a free array sorting
   * 
   * @return {String}
   * @scope protected
   */
  var __toString = function() {
      return String(this.text);
  }
  /**
   *  Check all visible options and set the state for them
   *
   *  @param {RegExp, String, Array} reg expression to match option text against
   *  @param {String} type state to be changed, currently 'selected' and 'visible' supported
   *  @param {Boolean} state true means 'set selected'
   *  @param {Boolean} pre true means 'preserve already selected options state'
   *  @param {String} match match needle from: 'start', 'end', 'exact', 'any'. Default is 'start'. Has no meaning, when regexp is supplied.
   *  @param {String} attr attribute to do the check against, either 'text' or 'value'
   *  @return {Number} number of changed options
   */
  var __setOptionsState = function ( reg   /* :RegExp, String, Array */
                                    ,type  /* :String */
                                    ,state /* :Boolean */
                                    ,pre   /* :Boolean */
                                    ,match /* :String */
                                    ,attr  /* :String */ ) /* :Number */ {
    var res = 0;
    /*
    *  either 'text' or 'value' is allowed
    */
    if (attr != 'value') attr = 'text';
    /*
    *  don't touch options with empty match 
    */
    if (!isRegExp(reg)) {
        reg = RegExp.escape(reg);
        if (!isString(match)) match = 'start';
        switch (match.toLowerCase()) {
            case "any" :
                reg = new RegExp(reg,"i");   
                break;
            case "end" :
                reg = new RegExp(reg+"$","i");   
                break;
            case "exact" :
                reg = new RegExp("^"+reg+"$","i");   
                break;
            case "start" :
            case "default" :
                reg = new RegExp("^"+reg,"i");   
                break;
        }
    }
    for (var i=0, oL=options.length; i<oL; i++) {
      /*
      *  skip already options with matching state, if allowed
      */
      if (pre && options[i][type] == state) continue;
      /*
      *  flag that some options are processed successfully
      */
      if ( reg.test(options[i][attr]) ) {
        res += (options[i][type] != state);
        options[i][type] = state;
      } else {
        if ( options[i][type] == state && !pre) {
          res += (options[i][type] == state);
          options[i][type] = ! state;
        }
      }
    }
    /*
    *  if something has changed, do the update
    */
    if (res) self.updateOptions();
    return res;
  }
  /**
   *  Stores actual selection state, made by user
   *
   *  @scope protected
   */
  var __storeOptionsState = function () /* :void */{
      for (var i=0, oL=this.options.length; i<oL; i++) {
          if (!isUndefined(this.options[i].__idx) && options[this.options[i].__idx]) { 
              options[this.options[i].__idx].selected = this.options[i].selected;
              options[this.options[i].__idx].defaultSelected = this.options[i].defaultSelected;
          }
      }
  }
  /**
   *  Stores option name in the tracker
   * 
   *  @see #trackers
   *  @param {String} track name of the tracker
   *  @param {String} text option text
   *  @param {String} val option value
   *  @return {Boolean}
   *  @scope private
   */
  var __saveOptionsTrack = function (track /* :String */, text /* :String */, val /* :String */) /* :Boolean */{
      if (!trackers[track]) return false;
      if (trackers[track].fixed) {
          trackers[track] = [];
          trackers[track].fixed = false;
      }  
      trackers[track][trackers[track].length] = {'text' :text, 'value' :val};
      return true;  
  }
  /**
   *  Fixes current tracking state in the past
   *  
   *  @scope private
   */
  var __fixOptionsTrack = function () {
      trackers.added.fixed = true;
      trackers.deleted.fixed = true;
  }
  //---------------------------------------------------------------------------
  // Public methods
  //---------------------------------------------------------------------------
  /**
   *  Return the number of options in the list
   *
   *  @return {Number}
   *  @scope public
   */
  this.hasOptions = function () /* :Number */{
    return options.length;
  }
  /**
   *  Redraw options list
   *
   *  @scope public
   */
  this.updateOptions = function () /* :void */ {
      /*
      *  detach box to speedup conversion
      */
      var nxt = node.nextSibling
         ,prn = node.parentNode;
      if (prn) prn.removeChild(node);
      
      for (var i=ndx=0, oL=options.length; i<oL; i++) {
          /*
          *  don't process options beyond the borders
          */
          if (ndx>=maxlistlen) break;
          if (options[i].visible) {
              if (!node.options[ndx]) {
                  node.options[ndx] = new Option(options[i].text
                                                ,options[i].value
                                                ,options[i].defaultSelected
                                                ,options[i].selected);
              } else {
                  /*
                  *  firefox behavior - it does reset selectbox, 
                  *  when even single label has been touched
                  *  so, try to keep its scroll on the place
                  */
                  if (node.options[ndx].text != options[i].text)
                      node.options[ndx].text        = options[i].text;
    
                  node.options[ndx].value           = options[i].value;
                  node.options[ndx].defaultSelected = options[i].defaultSelected;
                  node.options[ndx].selected        = options[i].selected;
              }
              node.options[ndx].__idx           = i;
              ndx++;
          }
      }

      node.options.length=ndx;

      __fixOptionsTrack();
      /*
      *  set box back on its place
      */
      if (prn) {
          prn.insertBefore(node, nxt);
          prn = nxt = null;
      }
  }
  //-------------------------------------------
  //  SELECTION OPERATION
  //-------------------------------------------
  /**
   *  Selects all available options
   *
   *  It's useful, when do the transfer from one select box to another and before submitting a form
   *
   *  @scope public
   */
  this.selectAllOptions = function () /* :void */ {
    for (var i=0; i<options.length; i++) {
       if (options[i].visible) {
         options[i].selected = true;
       }
    }
    this.updateOptions();
  }
  /**
   *  Selects all matching options
   *
   *  @param {String} needle search phrase
   *  @param {String} match match needle from: 'start', 'end', 'exact', 'any'. Default is 'start'. Has no meaning, when regexp is supplied.
   *  @param {String} attr attribute to do the check against, either 'text' or 'value'
   *  @return {Number} number of selected options 
   *  @scope public
   */
  this.selectMatchingOptions = function (needle /* :String */, match /* :String */, attr /* :String */) /* :Number */ {
    return __setOptionsState (needle, 'selected', true, true, match, attr);
  }
  /**
   *  Selects all matching options and reset selection for others
   *
   *  @param {String} needle search phrase
   *  @param {String} match match needle from: 'start', 'end', 'exact', 'any'. Default is 'start'. Has no meaning, when regexp is supplied.
   *  @param {String} attr attribute to do the check against, either 'text' or 'value'
   *  @return {Number} number of selected options 
   *  @scope public
   */
  this.selectOnlyMatchingOptions = function (needle /* :String */, match /* :String */, attr /* :String */) /* :Number */ {
    return __setOptionsState (needle, 'selected', true, false, match, attr);
  }
  /**
   *  Removes selection from all available options
   *
   *  @scope public
   */
  this.unselectAllOptions = function () /* :void */ {
    for (var i=0, nL=node.options.length; i<nL; i++) {
//         node.options[i].selected = false;
         options[node.options[i].__idx].selected = false;
    }
    node.selectedIndex = -1;
  }
  /**
   *  Selects all matching options
   *
   *  @param {String} needle search phrase
   *  @param {String} match match needle from: 'start', 'end', 'exact', 'any'. Default is 'start'. Has no meaning, when regexp is supplied.
   *  @param {String} attr attribute to do the check against, either 'text' or 'value'
   *  @return {Number} number of unselected options 
   *  @scope public
   */
  this.unselectMatchingOptions = function (needle /* :String */, match /* :String */, attr /* :String */) /* :Number */ {
    return __setOptionsState (needle, 'selected', false, true, match, attr);
  }
  /**
   *  Removes selection from all matching options and set for others
   *
   *  @param {String} needle search phrase
   *  @param {String} match match needle from: 'start', 'end', 'exact', 'any'. Default is 'start'. Has no meaning, when regexp is supplied.
   *  @param {String} attr attribute to do the check against, either 'text' or 'value'
   *  @return {Number} number of selected options 
   *  @scope public
   */
  this.unselectOnlyMatchingOptions = function (needle /* :String */, match /* :String */, attr /* :String */) /* :Number */ {
    return __setOptionsState (needle, 'selected', false, false, match, attr);
  }
  //-------------------------------------------
  //  FIXATION OPERATION
  //-------------------------------------------
  /**
   *  Fix positions of all matching options
   *
   *  @param {String} needle search phrase
   *  @param {String} match match needle from: 'start', 'end', 'exact', 'any'. Default is 'start'. Has no meaning, when regexp is supplied.
   *  @param {String} attr attribute to do the check against, either 'text' or 'value'
   *  @return {Number} number of fixed options 
   *  @scope public
   */
  this.fixMatchingOptions = function (needle /* :String */, match /* :String */, attr /* :String */) /* :Number */ {
    return __setOptionsState (needle, 'fixed', true, true, match, attr);
  }
  /**
   *  Fix positions of all matching options and reset selection for others
   *
   *  @param {String} needle search phrase
   *  @param {String} match match needle from: 'start', 'end', 'exact', 'any'. Default is 'start'. Has no meaning, when regexp is supplied.
   *  @param {String} attr attribute to do the check against, either 'text' or 'value'
   *  @return {Number} number of fixed options 
   *  @scope public
   */
  this.fixOnlyMatchingOptions = function (needle /* :String */, match /* :String */, attr /* :String */) /* :Number */ {
    return __setOptionsState (needle, 'fixed', true, false, match, attr);
  }
  /**
   *  Unfixes positions of all matching options
   *
   *  @param {String} needle search phrase
   *  @param {String} match match needle from: 'start', 'end', 'exact', 'any'. Default is 'start'. Has no meaning, when regexp is supplied.
   *  @param {String} attr attribute to do the check against, either 'text' or 'value'
   *  @return {Number} number of unfixed options 
   *  @scope public
   */
  this.unfixMatchingOptions = function (needle /* :String */, match /* :String */, attr /* :String */) /* :Number */ {
    return __setOptionsState (needle, 'fixed', false, true, match, attr);
  }
  /**
   *  Unfixes positions of all matching options and set for others
   *
   *  @param {String} needle search phrase
   *  @param {String} match match needle from: 'start', 'end', 'exact', 'any'. Default is 'start'. Has no meaning, when regexp is supplied.
   *  @param {String} attr attribute to do the check against, either 'text' or 'value'
   *  @return {Number} number of unfixed options 
   *  @scope public
   */
  this.unfixOnlyMatchingOptions = function (needle /* :String */, match /* :String */, attr /* :String */) /* :Number */ {
    return __setOptionsState (needle, 'fixed', false, false, match, attr);
  }
  //-------------------------------------------
  //  VISIBILITY OPERATION
  //-------------------------------------------
  /**
   *  Shows all matching options
   *
   *  @param {String} needle search phrase
   *  @param {String} match match needle from: 'start', 'end', 'exact', 'any'. Default is 'start'. Has no meaning, when regexp is supplied.
   *  @param {String} attr attribute to do the check against, either 'text' or 'value'
   *  @return {Number} number of shown options 
   *  @scope public
   */
  this.showMatchingOptions = function (needle /* :String */, match /* :String */, attr /* :String */) /* :Number */ {
    return __setOptionsState (needle, 'visible', true, true, match, attr);
  }
  /**
   *  Shows all matching options and hide others
   *
   *  @param {String} needle search phrase
   *  @param {String} match match needle from: 'start', 'end', 'exact', 'any'. Default is 'start'. Has no meaning, when regexp is supplied.
   *  @param {String} attr attribute to do the check against, either 'text' or 'value'
   *  @return {Number} number of shown options 
   *  @scope public
   */
  this.showOnlyMatchingOptions = function (needle /* :String */, match /* :String */, attr /* :String */) /* :Number */ {
    return __setOptionsState (needle, 'visible', true, false, match, attr);
  }
  /**
   *  Hides all matching options
   *
   *  @param {String} needle search phrase
   *  @param {String} match match needle from: 'start', 'end', 'exact', 'any'. Default is 'start'. Has no meaning, when regexp is supplied.
   *  @param {String} attr attribute to do the check against, either 'text' or 'value'
   *  @return {Number} number of hidden options 
   *  @scope public
   */
  this.hideMatchingOptions = function (needle /* :String */, match /* :String */, attr /* :String */) /* :Number */ {
    return __setOptionsState (needle, 'visible', false, true, match, attr);
  }
  /**
   *  Hides all matching options and show others
   *
   *  @param {String} needle search phrase
   *  @param {String} match match needle from: 'start', 'end', 'exact', 'any'. Default is 'start'. Has no meaning, when regexp is supplied.
   *  @param {String} attr attribute to do the check against, either 'text' or 'value'
   *  @return {Number} number of hidden options 
   *  @scope public
   */
  this.hideOnlyMatchingOptions = function (needle /* :String */, match /* :String */, attr /* :String */) /* :Number */ {
    return __setOptionsState (needle, 'visible', false, false, match, attr);
  }
  //-------------------------------------------
  //  MISC FUNCTIONS
  //-------------------------------------------
  /**
   *  Sorts the options
   *
   *  @scope public
   */
  this.sort = function ()/* :void */{
    options = options.sort();
    this.updateOptions();
  }
  //-------------------------------------------
  //  GETTERS
  //-------------------------------------------
  /**
   *  Returns current selectbox value
   * 
   *  @return {String}
   *  @scope public 
   */
  this.getValue = function () {
      return node.value;
  }
  /**
   *  Return visible option or its property, if exists
   * 
   *  @param {Number} id option id
   *  @param {Object} p optional property name
   *  @scope public 
   */
  this.getOption = function(id /* :Number */, p /* :String */) /* :String,Array */ {
      if (!isNumeric(id) || id<0 || !node.options[id]) return "";
      return isString(p)?node.options[id][p]:[text.value,node.options[id].value];
  }
  /**
   *  Returns a list of the selected options
   * 
   *  @return {Array}
   *  @scope public
   */
  this.getSelectedOptions = function () /* :Array */ {
      var res = [];
      for (var i=0, oL=node.options.length; i<oL; i++) {
          if (node.options[i].selected) {
              res[res.length] = [node.options[i].text, node.options[i].value];
          }
      }
      return res;      
  }
  /**
   *  Returns the number of options in the box
   * 
   *  @return {Number}
   *  @scope public 
   */
  this.getOptionsLength = function () /* :Number */ {
      return node.options.length; 
  }
  /**
   *  Returns the selected index
   * 
   *  @return {Number}
   *  @scope public
   */
  this.getSelectedIndex = function () /* :Number */ {
      return node.selectedIndex;
  }  
  /**
   *  Returns ID of the element, it is bound to
   * 
   *  @return {String} 
   *  @scope public
   */
  this.getId = function () /* :String */ {
      return id;
  }
  /**
   *  Returns the element, it is bound to
   * 
   *  @return {HTMLSelectElement}
   *  @scope public
   */
  this.getEl = function () /* :HTMLSelectElement */ {
      return node;
  }
  /**
   *  Returns arary of options title text
   * 
   *  @param {Array} ids optional list of ids to get names of  
   *  @return {Array} list of options names
   *  @scope public
   */
  this.getOptionsNames = function (ids /* :Array */) /* :Array */ {
      if (ids) ids = new RegExp("^("+RegExp.escape(ids)+")$","i");
      var tmp = [];
      for (var i=0, oL=options.length; i<oL; i++) {
          if (isUndefined(ids) || (isRegExp(ids) && ids.test(options[i].value))) {
              tmp[tmp.length] = options[i].text;
          }
      }
      return tmp;
  }
  /**
   * Returns requested options track
   * 
   * @param {String} type one of 'all', 'added', 'deleted' 
   * @param {String} part optional 'text' or 'value'
   * @param {String} j optional list joiner, has no meaning if 'part' is not specified
   * @return {String} joined options track
   * @scope public
   */
  this.getOptionsTrack = function ( type /* :String */
                                   ,part /* :String */
                                   ,j /* :String */) /* :String */{
    var tmp = [];
    if ('text' != part && 'value' != part) part = false;
    if ('all' == String(type).toLowerCase()) {
        for (var i=0, tL=options.length; i<tL; i++) {
            tmp[tmp.length] = part?options[i][part]:options[i];
        }
    } else if (trackers[String(type).toLowerCase()]) {
        for (var i=0, tL=trackers[type].length; i<tL; i++) {
            tmp[tmp.length] = part?trackers[type][i][part]:trackers[type][i]; 
        }
    }
    return j&&part?tmp.join(j):tmp;
  }
  //-------------------------------------------
  //  OPTIONS MANIPULATION
  //-------------------------------------------
  /**
   *  Removes all matching options, preserving the selection
   *
   *  @param {String} needle search phrase
   *  @param {String} match match needle from: 'start', 'end', 'exact', 'any'. Default is 'start'. Has no meaning, when regexp is supplied.
   *  @param {String} attr attribute to do the check against, either 'text' or 'value'
   *  @scope public
   */
  this.removeMatchingOptions = function (needle /* :String */, match /* :String */, attr /* :String */) {
      var tmp = self.getSelectedOptions().flatten([0]);
      self.selectOnlyMatchingOptions(needle, match, attr);
      self.removeSelectedOptions();
      if (tmp.length>0) self.selectOnlyMatchingOptions(tmp, 'exact');
  }
  /**
   *  Removes all selected options from the list
   *
   *  @return {Array} list of deleted options
   *  @scope public
   */
  this.removeSelectedOptions = function () /* :Array */{
    var res = [], tmp;
    for (var i=(options.length-1); i>=0; i--) { 
      if (options[i].selected && (tmp = self.removeOption (i, false)))
          res[res.length] = tmp;
    }
    if (res) this.updateOptions();
    node.selectedIndex = -1;
    return res;
  }
  /**
   *  Removes all the options
   *
   *  @return {Array} list of deleted options
   *  @scope public
   */
  this.removeAllOptions = function () /* :Array */{
    var res = [];  
    for (var i=(options.length-1); i>=0; i--) { 
        __saveOptionsTrack ('deleted', options[i].text, options[i].value);
        res[res.length] = options[i];
    }
    node.selectedIndex = -1;
    node.options.length = options.length = 0;
    return res;
  }
  /**
   *  Removes the option with specified index
   *
   *  @param {Number} idx option index
   *  @param {Boolean} update after removal
   *  @return {Boolean} removal state
   *  @scope public
   */
  this.removeOption = function ( idx /* :Number */
                                ,update /* :Boolean */) /* :Boolean */{
    if (!options[idx]) return false;
    /*
    * update the track 
    */
    __saveOptionsTrack ('deleted', options[idx].text, options[idx].value);
    /*
    * and delete 
    */
    var res = options[idx]; 
    options.splice(idx,1);
    if (false !== update) this.updateOptions();
    return res;
  }
  /**
   *  Adds options to the list
   *
   *  @param {String} text option title
   *  @param {String} value option value
   *  @param {Boolean} selected selection state
   *  @param {Boolean} defaultSelected selection state
   *  @param {Boolean} sort
   *  @param {Boolean} update if 'false' is not explicitly specified, option is added immediately
   *  @return {Boolean} addition state
   */
  this.addOption = function ( text /* :String */
                             ,value /* :String */
                             ,defaultSelected /* :Boolean */
                             ,selected /* :Boolean */
                             ,sort /* :Boolean */
                             ,update /* :Boolean */) /* :Boolean */ {
    if (!isScalar(text) || !isScalar(value)) return false;
    if (isUndefined(update)) update = true;
    options[options.length] = { 'text'  : text
                               ,'value' : value
                               ,'defaultSelected' : defaultSelected
                               ,'selected' : selected
                               ,'visible'  : true
                               ,'fixed'    : false
                              };
    if (update && node.options.length<maxlistlen) {
        node.options[node.options.length] = new Option ( text ,value ,defaultSelected ,selected);
        node.options[node.options.length-1].__idx = options.length-1;
    }
    /*
    *  overload the 'toString' method  
    */
    options[options.length-1].toString = __toString;
    /*
    * update the track 
    */
    __saveOptionsTrack ('added', text, value);
    if (sort) this.sort();
    
    return true;
  }
  /**
   *  Adds the array of the options to the list
   *
   *  @param {Array} list array of the options
   *  @param {Boolean} sort
   *  @return {Boolean}
   *  @scope public
   */
  this.addOptionsList = function ( list /* :Array */
                                  ,sort /* :Boolean */ ) /* :Boolean */ {
   
    if (isUndefined(list) || !isNumeric(list.length) || list.length<1) return false;

    for (var i=0, lL=list.length; i<lL; i++) {
        var li = list[i];
        if (li.text) self.addOption (li.text, li.value?li.value:li.text
                                    ,Boolean(li.selected), Boolean(li.defaultSelected)
                                    ,false, false);
        else if (isScalar(li)) self.addOption (li, li
                                              ,false, false
                                              ,false, false);
        else if (li[0]) self.addOption (li[0], li[1]?li[1]:li[0]
                                       ,Boolean(li[2]), Boolean(li[3])
                                       ,false, false);
    }
    if (sort) this.sort();
    else this.updateOptions();

    return true;
  }
  //-------------------------------------------
  //  OPTIONS TRANSFER
  //-------------------------------------------
  /**
   *  Copies selected options to another select box
   *  
   *  @param {SelectBox} to SelectBox to copy options to
   *  @param {Boolean} autosort enables autosort on update
   *  @param {RegExp} regex string to keep matching options
   *  @return {Boolean} move state
   */ 
  this.copySelectedOptions = function ( to /* :Selectbox*/
                                       ,regex /* :RegExp */
                                       ,autosort /* :Boolean */) /* :Boolean */{
    if (!to || !to.addOption) return false;
    /*
    * Unselect matching options, if required
    */
    if (regex) {
      self.unselectMatchingOptions(regex);
    }
    /*
    * Copy them over
    */
    for (var i=0, oL=options.length; i<oL; i++) {
      if (options[i].selected && options[i].visible) {
        to.addOption (options[i].text, options[i].value, options[i].selected, options[i].defaultSelected);
      }
    }
    if (autosort) {
      to.sort();
    } else {
      to.updateOptions();
    }
    return true;
  }
  /**
   *  Copies all options to another SelectBox
   *
   *  @param {SelectBox} to SelectBox to copy options to
   *  @param {Boolean} autosort enables autosort on update
   *  @param {RegExp} regex string to keep matching options
   *  @return {Boolean} move state
   */
  this.copyAllOptions = function ( to /* :Selectbox*/
                                  ,regex /* :RegExp */
                                  ,autosort /* :Boolean */) /* :Boolean */{
    if (!to || !to.addOption) return false;
    self.selectAllOptions();
    self.copySelectedOptions(to,regex,autosort);
  }
  /**
   *  Moves selected options to another select box
   *  
   *  This function moves options between select boxes. Works best with             
   *  multi-select boxes to create the common Windows control effect.               
   *  Passes all selected values from the first object to the second                
   *  object and re-sorts each box.                                                 
   *  If a third argument of 'false' is passed, then the lists are not              
   *  sorted after the move.                                                        
   *  If a fourth string argument is passed, this will function as a                
   *  Regular Expression to match against the TEXT or the options. If               
   *  the text of an option matches the pattern, it will NOT be moved.              
   *  It will be treated as an unmoveable option.                                   
   *  You can also put this into the <SELECT> object as follows:                    
   *    onDblClick="moveSelectedOptions(this,this.form.target)                      
   *  This way, when the user double-clicks on a value in one box, it               
   *  will be transferred to the other (in browsers that support the                
   *  onDblClick() event handler).                                                  
   *
   *  @param {SelectBox} to SelectBox to copy options to
   *  @param {Boolean} autosort enables autosort on update
   *  @param {RegExp} regex string to keep matching options
   *  @return {Boolean} move state
   */ 
  this.moveSelectedOptions = function ( to /* :Selectbox*/
                                       ,regex /* :RegExp */
                                       ,autosort /* :Boolean */) /* :Boolean */{
    if (!to || !to.addOption) return false;
    /*
    * Unselect matching options, if required
    */
    if (regex) {
      self.unselectMatchingOptions(regex);
    }
    /*
    * Reset selection on the target box 
    */
    to.unselectAllOptions();

    /*
    * Move them over
    */
    var opts = self.removeSelectedOptions(false);
    
    for (var i=opts.length-1; i>=0; i--) {
        to.addOption (opts[i].text, opts[i].value, opts[i].defaultSelected, opts[i].selected);
    }
    if (autosort) {
      to.sort();
      self.sort();
    } else {
      to.updateOptions();
      self.updateOptions();
    }
    return true;
  }
  /**
   *  Move all options to another SelectBox
   *
   *  @param {SelectBox} to SelectBox to copy options to
   *  @param {Boolean} autosort enables autosort on update
   *  @param {RegExp} regex string to keep matching options
   *  @return {Boolean} move state
   */
  this.moveAllOptions = function ( to /* :Selectbox*/
                                  ,regex /* :RegExp */
                                  ,autosort /* :Boolean */) /* :Boolean */{
    if (!to || !to.addOption) return false;
    self.selectAllOptions();
    self.moveSelectedOptions(to,regex,autosort);
  }
  //-------------------------------------------
  //  OPTIONS MOVEMENT
  //-------------------------------------------
  /**
   *  Swap 2 options
   *
   *  @param {Number} idx1
   *  @param {Number} idx2
   *  @return {Boolean} swap state
   *  @scope public
   */
  this.swapOptions = function ( idx1 /* :Number */
                               ,idx2 /* :Number */) /* :Boolean */ {
    if (!options[idx1] || options[idx2]) return false;
    var tmp = options[idx1];
    options[idx1] = options[idx2];
    options[idx2] = tmp;
    this.updateOptions();
  }
  /**
   *  Moves specified option up
   *
   *  @return {Boolean} operation state
   *  @scope public
   */
   this.moveSelectedOptionsUp = function () /* :Boolean */{
     var res = false;
     for (i=0, oL=options.length; i<oL; i++) {
       if (options[i].selected && options[i].visible) {
         if (i != 0 && !options[i-1].selected && !options[i-1].fixed) {
           this.swapOptions(i,i-1);
           obj.options[i-1].selected = true;
         }
       }
     }
   }
  /**
   *  Moves specified option up
   *
   *  @return {Boolean} operation state
   *  @scope public
   */
   this.moveSelectedOptionsUp = function () /* :Boolean */{
     var res = false;
     for (oL=i=options.length-1; i>=0; i--) {
       if (options[i].selected && options[i].visible) {
         if (i != oL && !options[i+1].selected && !options[i+1].fixed) {
           this.swapOptions(i,i+1);
           obj.options[i+1].selected = true;
         }
       }
     }
   }
  //-------------------------------------------
  //  SELECTION MOVEMENT
  //-------------------------------------------
  /**
   *  Selects the option by it's id
   * 
   *  @param {Number} id option id
   *  @param {Boolean} force
   *  @return {Boolean}
   *  @scope public
   */
  this.selectOption = function (id /* :Number */, force) {
      if (!isNumber(id) || (!force && (id<0 || !node.options[Math.abs(id)]))) return false;
      self.unselectAllOptions();
      if (id >= node.options.length) {
          id = node.options.length-1;
      } else if (id < 0) {
          id = 0;
      }
      node.options[id].selected = true;
      options[node.options[id].__idx].selected = true;
      return true;
  }
  /**
   *  Moves selection 1 position higher on the list, allows to cycle movement
   *  Don't use __updateOptions here, because it affects the visual performance
   * 
   *  @param {Boolean} cyclic allows to move selection from the beginning to the end of the list
   *  @scope public
   */
  this.selectPrev = function (cycle /* :Boolean */) /* :void */{
     var res = false
        ,selected = false;
     for (i=node.options.length-1; i>=0; i--) {
       if (selected || node.options[i].selected) {
           var tmp = node.options[i].selected;
           node.options[i].selected = selected;    
           options[node.options[i].__idx].selected = selected;
           selected = tmp; 
       }
     }
     if (selected) {
         if (cycle) {
             for (i=node.options.length-1; i>=0; i--) {
                 node.options[0].selected = false;
                 node.options[i].selected = true;
                 options[node.options[0].__idx].selected = false;
                 options[node.options[i].__idx].selected = true;
                 break;
             }
         } else {
             node.options[0].selected = true;             
             options[node.options[0].__idx].selected = true;
         }
     }
  }
  /**
   *  Moves selection 1 position lower on the list, allows to cycle movement
   *  Don't use __updateOptions here, because it affects the visual performance
   * 
   *  @param {Boolean} cyclic allows to move selection from the end to the beginning of the list
   *  @scope public
   */
  this.selectNext = function (cycle /* :Boolean */) /* :void */ {
     var res = false
        ,selected = false;
     for (i=0, oL=node.options.length; i<oL; i++) {
       if (selected || node.options[i].selected) {
           var tmp = node.options[i].selected;
           node.options[i].selected = selected;
           options[node.options[i].__idx].selected = selected;
           selected = tmp; 
       }
     }
     if (selected) {
         if (cycle) {
             for (i=0, oL=node.options.length; i<oL; i++) {
                 options[node.options[oL-1].__idx].selected = false;
                 options[node.options[i].__idx].selected = true;
                 node.options[oL-1].selected = false;
                 node.options[i].selected = true;
                 break;
             }
         } else {
             node.options[oL-1].selected = true;
             options[node.options[oL-1].__idx].selected = true;
         }
     }
  }
   /*
   * let's call a constructor  
   */
  __construct(id);
}